Pinvon's Blog

所见, 所闻, 所思, 所想

Go实战(四) 复合数据类型

学习自: Go 语言圣经

概述

数组和结构体有固定内存大小的数据结构; slice 和 map 是动态的数据结构, 会根据需要而动态增长.

数组

数组的长度固定, 我们一般不直接使用数组. slice 提供的功能会更多, 但我们要先理解数组, 才能理解 slice 的原理.

数组下标从 0 开始, len() 返回数组元素个数.

初始化

默认每个元素都被初始化为元素类型所对应的零值. 也可以自己赋予初始值:

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 3}
fmt.Println(r[2])  // 0

可以根据初始化元素的个数来决定数组的长度:

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q)  // [3]int

数组长度是数组类型的一部分, 因此下面的赋值是错误的:

q := [3]int{1,2,3}
q = [4]int{1,2,3,4}  // 编译错误

另外, 数组长度必须是常量表达式, 因为数组长度在编译期确定.

索引和对应的值

我们可以在数组里存放索引-值, 做法如下:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

如果有索引未使用, 可以直接省略, Go 会使用零值来代替:

r := [...]int{99:-1}

从 0 到 98 的索引对应的值都是 0; 索引为 99 对应的值是 -1.

数组运算

如果数组元素类型是可比较的, 且两个数组的所有元素都相等, 则这两个数组是相等的.

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

遍历

var a [3]int             // array of 3 integers
fmt.Println(a[0])        // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]

// Print the indices and elements.
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// Print the elements only.
for _, v := range a {
    fmt.Printf("%d\n", v)
}

slice

slice 的语法和数组很像, 但不固定长度.

slice 的数据结构

slice 的底层引用了一个数组对象.

一个 slice 由三部分构成: 指针, 长度, 容量.

  1. 指针: 指向第一个 slice 元素对应的底层数组元素的地址.(注: slice 的第一个元素不一定是数组的第一个元素)
  2. 长度: slice 中的元素数目, 不能超过容量, 使用 len() 获得.
  3. 容量: 从 slice 的开始位置到底层数据的结尾位置, 使用 cap() 获得.

如下图所示, 多个 slice 之间可以共享底层数据, 并且引用的数组部分区间可以重叠.

7.png

从这个图中, 也可以看出, len() 指的是 slice 的长度, cap() 指的是从 slice1 开始到底层数组最后一个元素之间的长度.

slice 在初始化时不需要指定长度, 而数组需要指定长度. 但是 slice 的底层数据结构又是数组, 这个背后的原理是什么?

其实, 在初始化 slice 时, 会隐式创建一个合适大小的数组, 然后 slice 的指针指向底层的数组.

声明和初始化

  • 声明一个未指定大小的数组来定义切片: var name []type
  • 使用 make() 创建切片: var slice1 []type = make([]type, len)
  • slice1 := make([]type, len)
  • 可以初始化的时候就指定容量: slice1 := make([]type, len, cap)
  • 利用已有的数组来创建 slice: slice1 := array[m:n]

切片

假设 x 是 slice 类型的, 则对 x 进行切片, 可以写成: y := x[m:n].

其中, \(0 \leq m \leq n \leq cap(s)\). y 引用 x 从第 m 个元素到第 n-1 个元素之间的数据.

如果切片操作超过了 cap(x), 则会出现 panic:out of range 错误; 如果切片操作超过了 len(x), 则意味着对 x 进行扩展, 此时 y 里面的数据会比 x 还多.

slice 作为函数参数

因为 slice 的底层数据结构是数组, 且多个 slice 可以共享同一个底层数据结构, 因此, 对 slice 进行复制, 其实只是为底层数组创建了一个新的 slice 别名. 如果在函数内部对 slice 进行了修改, 会影响到函数外部. 如:

// reverse reverses a slice of ints in place.
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

a := [...]int{0, 1, 2, 3, 4, 5}  // a 是数组, a[:] 是 slice
reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"

比较

slice 不能像数组那样, 通过 == 操作符来判断两个 slice 是否含有全部相同的元素. 但有个例外: 如果这个 slice 是 []byte 类型的, 则可以使用 bytes.Equal() 来判断; 对于其他类型, 只能通过遍历每个元素来比较.

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
        if x[i] != y[i] {
            return false
        }
    }
    return true
}

slice 可以和 nil 作比较. slice 类型的零值就是 nil, 此时 slice 没有底层数组, len() 和 cap() 都是 0.

var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

可以看出, 如果要判断一个 slice 是否为空, 应该判断 len() 是否为 0, 而不能将其与 nil 作比较. 因为一个 nil 值的 slice 的行为与其他任意 0 长度的 slice 是一样的.

append()

append() 用于向 slice 添加新元素. 如:

var runes []rune
for _, r := range "Hello, 世界" {
    runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"

当然, 这一段代码只是用来演示 append() 的用法, 其实际效果与 runes = []rune("Hello, 世界") 一样.

append() 原理

假设 appendInt() 就是 []int 类型的 slice 的 append() 操作.

func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        copy(z, x) // a built-in function; see text
    }
    z[len(x)] = y
    return z
}

通过代码可以看出来, 每次添加新元素到 x 时, 都会先判断 len(x) 是否有足够空间来容纳新元素.

如果足够容纳, 则直接扩展 x, 然后将新元素添加进来, 添加新元素后的 slice 与原来的 slice 共享同一个数组;

如果空间不足, 则创建一个新的 slice, 其 cap() 至少要设置成添加新元素后的长度的两倍. 然后再将所有元素放进新的 slice 中, 添加新元素后的 slice 与原来的 slice 不共享同一数组.

copy() 是 Go 语言内置的函数, 可以将第二个参数复制给第一个参数.

内置的 append() 的实现会更加复杂, 它还可以追加多个元素, 甚至是另一个 slice. 如(注意最后一次追加时, 使用了省略号, 表示接收变长的 slice 参数):

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"

我们可以适当修改 appendInt() 来达到同样的目的:

func appendInt(x []int, y ...int) []int {
    var z []int
    zlen := len(x) + len(y)
    // ...expand z to at least zlen...
    copy(z[len(x):], y)
    return z
}

在 slice 原有内存空间修改元素

nonempty() 在原有 slice 内存空间上返回不包含空字符串的列表:

// Nonempty is an example of an in-place slice algorithm.
package main

import "fmt"

// nonempty returns a slice holding only the non-empty strings.
// The underlying array is modified during the call.
func nonempty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++
        }
    }
    return strings[:i]
}

...

data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data)           // `["one" "three" "three"]`

在这边, 输入的 slice 和输出的 slice 共享一个底层数组, 这样可以避免分配另一个数组, 不过原来的数据可能会被覆盖.

map

hash table 是一个无序的 k-v 对的集合, 所有的 k 都是不同的, 根据给定的 k, 可以在常数时间复杂度内检索, 更新或删除对应的 v. 在 Go 中, map 就是一个 hash table 的引用.

一个 map 类型的数据, 其所有的 k 必须是相同的类型, 所有的 v 也必须是相同的类型, 但 k 和 v 两者之间的类型可以不同. 另外, k 的类型要能支持 == 运算符, 这样可以判断 k 是否相等来判断是否已经存在. 最后, 浮点类型虽然可以作为 k, 但不是一个好选择, 因为直接比较浮点数, 得到的结果往往都是不相等.

创建 map 类型的数据

使用内置的 map() 创建 map:

ages := make(map[string]int)  // mapping from strings to ints

使用 map 字面值的语法创建 map:

ages := map[string]int{
    "alice": 31,
    "charlie": 34,
}

这相当于:

ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

创建空 map 的表达式:

ages := map[string]int{}

上面的初始化都是使用简短变量声明的. 如果先声明, 再创建, 可以这样:

var ages map[string]int
ages = make(map[string]int)

访问 map 元素

// 更新元素, 如果不存在该键, 则新增
ages["alice"] = 32

// 查询
fmt.Println(ages["alice"])  // "32"

// 删除元素
delete(ages, "alice")  // remove element ages["alice"]

这些操作都是安全的, 如果查询一个不存在的元素, 则会返回 v 的类型所对应的零值. 如, ages["bob"] 将返回 0.

但是, 如果 bob 键存在, 且对应的值就是 0, 程序中该如何判断这是 bob 键不存在而返回的 0, 还是原本就存在的 0?

age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }

或直接写成:

if age, ok := ages["bob"]; !ok { ... }

禁止取址操作

_ = &ages["bob"]  // compile error: cannot take address of map element

禁止对 map 元素取地址的原因在于, map 可能随着元素数量的增大而重新分配更大的空间, 从而可能导致之前的地址无效.

遍历

for name, age := range ages {
    fmt.Println("%s\t%d\n", name, age)
}

每次遍历的顺序都可能不同. 如果想按顺序遍历 key/value 对, 需要显式地对 key 进行排序. 如:

import "sort"

names := make([]string, 0, len(ages))
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

map 类型的零值

map 类型的零值为 nil.

大部分操作: 查找, 删除, len(), range循环都可以安全地工作在 nil 值的 map 上, 它们的行为和空 map 类似.

但是如果向 nil 值的 map 存入元素, 将会导致一个 panic 异常:

var ages map[string]int
fmt.Println(ages == nil)    // "true"
fmt.Println(len(ages) == 0) // "true"
ages["carol"] = 21 // panic: assignment to entry in nil map

修改办法是将 ages 的声明语句改成:

var ages map[string]int
ages = make(map[string]int)

// 或
ages := make(map[string]int)

所以要注意: 在向 map 存数据之前必须先创建 map.

比较

map 和 slice 一样, map 类型的数据相互之间不能用 == 来比较, 唯一的例外是可以与 nil 比较. 如果要判断两个 map 是否包含相同的 k-v 对, 需要通过循环实现:

func equal(x, y map[string]int) bool {
    if len(x) != len(y) {
        return false
    }
    for k, xv := range x {
        if yv, ok := y[k]; !ok || yv != xv {
            return false
        }
    }
    return true
}

在这个例子中, 使用 !ok 来区分元素不同和元素缺失.

slice 等不可比较的类型作为 map 的 Key

根据前面所述, 可以知道 map 的 Key 是要可以比较的类型. 但是如果我们想让 slice 或其他不可比较的类型作为 map 的 Key, 可以通过两个步骤来绕过这个限制, 以 slice 为例:

  • 定义辅助函数, 将 slice 转化为 string 类型.
  • 创建 map, 其中, key 为 string 类型, 每次对 map 操作时先用辅助函数将 slice 转化为 string.
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string)       { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }

k() 将输入的参数转化为 string 类型. 如果参数是整数类型, %q 占位符将其转化为单引号引起来的字符串, 如果参数是字符串或 slice, %q 占位符将其转化成双引号引起来的字符串.

把 map 当作 set 来使用

在 Go 语言中, 没有 set 类型. set 是指元素不重复的集合, 在 map 中, key 是不重复的, 所以如果忽略 value, 则 map 就相当于 set.

Value 的聚合

map 的 Value 可以是聚合的, 意思就是, map 的 Value 可能又是一个 map. 如:

var graph = make(map[string]map[string]bool)

func addEdge(from, to string) {
    edges := graph[from]
    if edges == nil {
        edges = make(map[string]bool)
        graph[from] = edges
    }
    edges[to] = true
}

func hasEdge(from, to string) bool {
    return graph[from][to]
}

struct

struct 是一种聚合类型, struct 类型的实体的成员可以由零个或多个任意类型的值聚合而成.

声明

type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee

这段代码声明了一个 Employee 结构体类型, 然后声明了一个 Employee 类型的变量 dilbert.

结构体成员的顺序很重要, 即使成员都相同, 但顺序不同, Go 也认为这两个结构体是不同的. 通常情况下, 结构体成员一行只写一个, 不过如果相邻两个成员的类型相同, 可以合并到一行.

访问 struct 变量的成员

使用点操作符对 struct 类型变量的成员进行访问. 如:

dilbert.Salary -= 5000 // demoted, for writing too few lines of code

还可以对成员取地址, 然后通过指针访问:

position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia

还可以定义结构体类型的变量的指针, 通过指针来访问变量的成员:

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

// 或者
(*employeeOfTheMonth).Position += " (proactive team player)"

成员

如果有个名为 S 的结构体类型, 则该结构体不能再包含 S 类型的成员. 即: 一个聚合的值不能包含它自身. 但是, 可以包含 *S 指针类型的成员. 这可以让我们创建递归的数据结构, 如链表和树等.

type tree struct {
    value       int
    left, right *tree
}

// Sort sorts values in place.
func Sort(values []int) {
    var root *tree
    for _, v := range values {
        root = add(root, v)
    }
    appendValues(values[:0], root)
}

// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
    if t != nil {
        values = appendValues(values, t.left)
        values = append(values, t.value)
        values = appendValues(values, t.right)
    }
    return values
}

func add(t *tree, value int) *tree {
    if t == nil {
        // Equivalent to return &tree{value: value}.
        t = new(tree)
        t.value = value
        return t
    }
    if value < t.value {
        t.left = add(t.left, value)
    } else {
        t.right = add(t.right, value)
    }
    return t
}

Go 语言中, 如果 struct 的成员名是以小写字母开头, 则其他包无法访问该成员.

struct 类型的零值

struct 类型的零值就是每个成员都是其对应的零值.

空 struct

struct{}

大小为 0.

初始化

根据成员的定义顺序初始化

type Point struct{ X, Y int }
p := Point{1, 2}

根据成员的名字来初始化(推荐)

type Point struct{ X, Y int }
p := Point{X: 1, Y: 2}

如果未提供 Y: 2, 则 Y 的初始值为零值.

由于结构体在使用时, 一般都是通过指针来引用的. 所以可以这样来创建并初始化一个结构体变量:

p := &Point{1, 2}

// 或
p := &Point{X: 1, Y: 2}

这种写法等价于:

p := new(Point)

*p = Point{1, 2}
// 或
*p = Point{X: 1, Y: 2}

struct 作为函数参数或返回值

func Scale(p Point, factor int) Point {
    return Point{p.X * factor, p.Y * factor}
}
fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"

但是这种直接传递的方式, 效率较低. 一般更推荐使用指针的方式传递参数.

在 Go 中, 如果不指定使用指针传参, 都会被默认为使用值传递, 即函数内部使用的参数, 其实是传递进去的参数的一份拷贝, 在函数内部所做的修改, 影响不到外面. 所以, 可以看出, 当 struct 较大时, 值传递要消耗较多内存, 因为值传递要另外拷贝一份 struct.

可以这样声明函数:

func Bonus(e *Employee, percent int) int {
    return e.Salary * percent / 100
}

struct 比较

如果 struct 的全部成员都是可比较的, 则 struct 也是可比较的.

struct 嵌套

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point
    Radius int
}

type Wheel struct {
    Circle Circle
    Spokes int
}

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

匿名嵌入

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

匿名成员是指那些只给出了类型, 而没给出名字的成员.

从上面的代码中可以看出, 访问匿名成员时, 会自顶向下, 一级一级往下找, 直到找到最后一个匿名成员, 然后将值赋给该类型的变量. 如, w.X 是找到 w.Circle.Point.X, 而不是找到 w.Circle.X.

由于匿名成员有隐式的名字, 所以不能同时包含两个类型相同的匿名成员, 否则会导致名字冲突.

在初始化时, 不能使用匿名成员的方式来初始化, 必须给出完整路径. 如:

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}

w.X = 42

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

JSON

JSON 是一种用于发送和接收结构化信息的标准协议.

JSON 的数据类型

在 JSON 中, 有数字, 布尔值, 字符串, 数组, 对象. 其中, 数组和对象是通过前三个类型组合而来的. 如下所示:

boolean         true
number          -273.15
string          "She said \"Hello, BF\""
array           ["gold", "silver", "bronze"]
object          {"year": 1980,
                 "event": "archery",
                 "medals": ["gold", "silver", "bronze"]}

marshaling(编码)

在 Go 中, 如果有一个 slice, 其元素都是 struct, 将这样的 slice 转化成 JSON 的过程, 就叫 marshaling(编组). 如:

有一个 struct, 名为 Movie:

type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}

有一个 slice, 其元素都是 Movie 类型:

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

使用 Marshal():

data, err := json.Marshal(movies)
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

打印结果:

[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]

如果觉得这种输出难以阅读, 可以使用 MarshalIndent():

data, err := json.MarshalIndent(movies, "", "    ")
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

打印结果如下:

[
    {
        "Title": "Casablanca",
        "released": 1942,
        "Actors": [
            "Humphrey Bogart",
            "Ingrid Bergman"
        ]
    },
    {
        "Title": "Cool Hand Luke",
        "released": 1967,
        "color": true,
        "Actors": [
            "Paul Newman"
        ]
    },
    {
        "Title": "Bullitt",
        "released": 1968,
        "color": true,
        "Actors": [
            "Steve McQueen",
            "Jacqueline Bisset"
        ]
    }
]

由于 Go 中 struct 的成员名如果是以小写字母开头, 则其他包无法访问, 所以 JSON 无法访问我们的 struct 里以小写字母开头的成员, 因此也就无法 marshaling, 不会出现在 JSON 编码的结果中.

所以到这里, 我们就可以明白, 为什么上面的例子中, struct 的成员都是大写字母开头的.

另外, 对比 Movie 的成员名和 marshaling 后 JSON 的成员名, 可以发现, Year 变成了 released, Color 变成了 color. 要知道为什么会这样, 需要先了解 结构体成员Tag 的概念.

结构体成员Tag

结构体成员Tag 是任意的字符串, 不过我们一般写成 key:"value" 这样的格式.

如果 key 为 json, 表示使用 encoding/json 包的编码和解码行为; 如果 key 为 xml, 表示使用 encoding/xml 包的编码和解码行为.

value 中的第一个值用于指定 JSON 对象的名字, 比如将 Go 语言中的 Year 对应到 JSON 中的 released.

如果 value 中还有第二个值, 表示额外的选项, omitempty 选项表示 Go 语言结构体成员为空或零值时不生成 JSON 对象. 由于电影 Casablanca 中 Color 为 false, 所以不输出 JSON 的 color 项.

unmarshaling(解码)

下面的例子将 JSON 格式的电影数据解码为一个 slice, 该 slice 的元素是 struct. 该例子中只解码 Title 成员:

var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
    log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

例子

通过例子来演示, 如何通过 HTTP 接口发送 JSON 格式的请求, 并返回 JSON 格式的信息.

定义合适的类型和常量:

// Package github provides a Go API for the GitHub issue tracker.
// See https://developer.github.com/v3/search/#search-issues.
package github

import "time"

const IssuesURL = "https://api.github.com/search/issues"

type IssuesSearchResult struct {
    TotalCount int `json:"total_count"`
    Items          []*Issue
}

type Issue struct {
    Number    int
    HTMLURL   string `json:"html_url"`
    Title     string
    State     string
    User      *User
    CreatedAt time.Time `json:"created_at"`
    Body      string    // in Markdown format
}

type User struct {
    Login   string
    HTMLURL string `json:"html_url"`
}

SearchIssues() 发出一个 HTTP 请求, 然后解码返回的 JSON 格式的结果:

package github

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "strings"
)

// SearchIssues queries the GitHub issue tracker.
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
    q := url.QueryEscape(strings.Join(terms, " "))
    resp, err := http.Get(IssuesURL + "?q=" + q)
    if err != nil {
        return nil, err
    }

    // We must close resp.Body on all execution paths.
    // (Chapter 5 presents 'defer', which makes this simpler.)
    if resp.StatusCode != http.StatusOK {
        resp.Body.Close()
        return nil, fmt.Errorf("search query failed: %s", resp.Status)
    }

    var result IssuesSearchResult
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        resp.Body.Close()
        return nil, err
    }
    resp.Body.Close()
    return &result, nil
}
// Issues prints a table of GitHub issues matching the search terms.
package main

import (
    "fmt"
    "log"
    "os"

    "gopl.io/ch4/github"
)

func main() {
    result, err := github.SearchIssues(os.Args[1:])
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%d issues:\n", result.TotalCount)
    for _, item := range result.Items {
        fmt.Printf("#%-5d %9.9s %.55s\n",
            item.Number, item.User.Login, item.Title)
    }
}

输出结果为:

$ go build gopl.io/ch4/issues
$ ./issues repo:golang/go is:open json decoder
13 issues:
#5680    eaigner encoding/json: set key converter on en/decoder
#6050  gopherbot encoding/json: provide tokenizer
#8658  gopherbot encoding/json: use bufio
#8462  kortschak encoding/json: UnmarshalText confuses json.Unmarshal
#5901        rsc encoding/json: allow override type marshaling
#9812  klauspost encoding/json: string tag not symmetric
#7872  extempora encoding/json: Encoder internally buffers full output
#9650    cespare encoding/json: Decoding gives errPhase when unmarshalin
#6716  gopherbot encoding/json: include field name in unmarshal error me
#6901  lukescott encoding/json, encoding/xml: option to treat unknown fi
#6384    joeshaw encoding/json: encode precise floating point integers u
#6647    btracey x/tools/cmd/godoc: display type kind of each named type
#4237  gjemiller encoding/base64: URLEncoding padding is optional

TEXT 模板 和 HTML 模板

text/template 和 html/template 等模板包提供了一个将变量值填充到一个 text 或 html 格式的模板的机制.

模板可以是一个文件, 也可以是一个字符串, 里面包含若干个 {{action}} 对象, action 将会触发其他的行为.

TEXT 模板

下面是一个简单的模板字符串:

const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`

每个 action 前都有一个点操作符".", 表示当前值, 也就是最初被初始化为调用模板时的参数.

{{range .Items}} 和 {{end}} 对应一个循环 action, 因此它们之间的内容可能会被展开多次.

"|" 操作符表示将前一个表达式的值作为后一个函数的参数.

Title 这一行的函数就是 printf(), 这个函数在所有模板中都可以直接使用.

Age 这一行的函数就是 daysAgo(), 函数原型如下:

func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
}

生成模板的输出有两个步骤:

  • 分析模板并转化为内部表示
  • 基于指定的输入执行模板

如下代码创建并分析上面定义的模板 templ:

report, err := template.New("report").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ)
if err != nil {
    log.Fatal(err)
}

首先使用 template.New() 创建并返回一个模板; 然后使用 Funcs() 将 daysAgo 等自定义函数注册到模板中; 最后调用 Parse() 分析模板.

接下来执行模板:

var report = template.Must(template.New("issuelist").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ))

func main() {
    result, err := github.SearchIssues(os.Args[1:])
    if err != nil {
        log.Fatal(err)
    }
    if err := report.Execute(os.Stdout, result); err != nil {
        log.Fatal(err)
    }
}

HTML 模板

暂时跳过, 用到再看.

Footnotes:

1

DEFINITION NOT FOUND.

Comments

使用 Disqus 评论
comments powered by Disqus